﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="EmbeddableSnmpAgent.cs" company="Data Ductus Malmö">
//   (c) 2012 Data Ductus Malmö AB
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace DataDuctus.Snmp.EmbeddableNetSnmp
{
  using System;
  using System.Diagnostics;
  using System.IO;
  using System.Runtime.InteropServices;
  using System.Threading;

  using DataDuctus.Snmp.EmbeddableNetSnmp.Internal;

  /// <summary>
  /// Extracts and fires up a net-snmp snmpd.exe agent and tears down on <see cref="Dispose"/>.
  /// </summary>
  public class EmbeddableSnmpAgent : IDisposable
  {
    #region Fields

    /// <summary>
    /// The hosting <see cref="Process"/> for snmpd.exe.
    /// </summary>
    private Process process;

    #endregion

    #region Constructors and Destructors

    /// <summary>
    /// Initializes a new instance of the <see cref="EmbeddableSnmpAgent"/> class.
    /// </summary>
    public EmbeddableSnmpAgent()
    {
      this.ProcessEndTimeout = 3000 /*3s*/;
      this.ServerPath = Path.Combine(Path.GetTempPath(), "embeddednetsnmp");
    }

    #endregion

    #region Public Properties

    /// <summary>
    /// Gets or sets the time to wait after trying to nicely shutdown snmpd.exe
    /// and thus <see cref="Process.Kill"/> it.
    /// </summary>
    public int ProcessEndTimeout { get; set; }

    /// <summary>
    /// Gets or sets the snmpd.exe path where it will be extraced and executed on.
    /// </summary>
    /// <remarks>
    /// If left blank, it will be extracted into windows temp directory.
    /// </remarks>
    public string ServerPath { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether <see cref="Trace"/> std err or not.
    /// </summary>
    public bool TraceStdErr { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether <see cref="Trace"/> std out or not.
    /// </summary>
    public bool TraceStdOut { get; set; }

    #endregion

    #region Public Methods and Operators
    /// <summary>
    /// Extracts the embedded mibs into specified <paramref name="mibPath"/>.
    /// </summary>
    /// <param name="mibPath">Where to extract the mibs to.</param>
    public static void ExtractMibs(string mibPath)
    {
      AgentExtractor.ExtractMibs(mibPath);
    }

    /// <summary>
    /// <see cref="Process.Dispose()"/>es the current snmpd.exe
    /// <see cref="Process"/> and frees up all resources allocated.
    /// </summary>
    /// <remarks>
    /// If the <see cref="Process.CloseMainWindow"/> do not work, it
    /// will use <see cref="Process.Dispose()"/> to destory the process.
    /// </remarks>
    public void Dispose()
    {
      if (null == this.process)
      {
        return;
      }

      try
      {
        if (!this.process.HasExited)
        {
          ShowWindow(this.process.MainWindowHandle, 5 /*SW_SHOW*/);

          if (!this.process.CloseMainWindow())
          {
            this.process.Kill();
          }

          this.process.WaitForExit(this.ProcessEndTimeout);
        }

        this.process.Dispose();
      }
      catch (Exception e)
      {
        Trace.TraceWarning(string.Format("Got exception when disposing the mongod server process msg = {0}", e.Message));
      }

      this.process = null;
    }

    /// <summary>
    /// Extracts and executes the snmpd.exe.
    /// </summary>
    public void Open()
    {
      KillNetSnmpProcesses(this.ProcessEndTimeout);

      var serverPath = this.ExtractServer();

      var conf = new ConfigFile { PersistenceBaseDirectory = this.ServerPath };

      conf.RoCommunities.Add("public");
      conf.RwCommunities.Add("private");

      conf.WriteConfig(Path.Combine(this.ServerPath, "snmpd.config"));

      this.process = new Process
        {
          StartInfo =
            {
              Arguments = this.RenderArguments(serverPath), 
              UseShellExecute = false, 
              ErrorDialog = false, 
              LoadUserProfile = false, 
              WindowStyle = ProcessWindowStyle.Hidden, 
              CreateNoWindow = false, 
              RedirectStandardError = true, 
              RedirectStandardInput = false, 
              RedirectStandardOutput = true, 
              FileName = serverPath /*since UseShellExecute=false*/, 
              WorkingDirectory = Path.GetDirectoryName(serverPath) ?? Directory.GetCurrentDirectory()
            }
        };

      this.process.OutputDataReceived += this.ProcessOutputDataReceived;
      this.process.ErrorDataReceived += this.ProcessErrorDataReceived;

      this.process.Start();

      this.process.BeginOutputReadLine();
      this.process.BeginErrorReadLine();

      while (this.process.MainWindowHandle == IntPtr.Zero)
      {
        this.process.Refresh();
      }

      ShowWindow(this.process.MainWindowHandle, 0 /*SW_HIDE*/);

      Thread.Sleep(1000 /*1s*/); // Just to give snmpd a chance to fire up...
    }

    #endregion

    #region Methods
    /// <summary>
    /// Tries to kill *all* snmpd.exe processes on current host.
    /// </summary>
    /// <param name="millisTimeout">
    /// The timeout to wait until <see cref="Process.Kill"/> is executed.
    /// </param>
    private static void KillNetSnmpProcesses(int millisTimeout)
    {
      Process[] processes = Process.GetProcessesByName("snmpd");

      foreach (Process proc in processes)
      {
        try
        {
          if (proc.HasExited)
          {
            continue;
          }

          ShowWindow(proc.MainWindowHandle, 5 /*SW_SHOW*/);

          if (!proc.CloseMainWindow())
          {
            proc.Kill();
          }

          proc.WaitForExit(millisTimeout);
        }
        catch (Exception e)
        {
          Trace.TraceWarning(string.Format("Got exception when killing mongod.exe msg = {0}", e.Message));
        }
      }
    }

    /// <summary>
    /// The show window.
    /// </summary>
    /// <param name="wnd">
    /// The wnd.
    /// </param>
    /// <param name="cmdShow">The show type.</param>
    /// <returns>
    /// The System.Boolean.
    /// </returns>
    [DllImport("user32.dll")]
    private static extern bool ShowWindow(IntPtr wnd, int cmdShow);

    /// <summary>
    /// Extracts all neccesary files to run snmpd.exe.
    /// </summary>
    /// <returns>A fully qualified file path to snmpd.exe.</returns>
    private string ExtractServer()
    {
      var mibPath = Path.Combine(this.ServerPath, "mibs");

      AgentExtractor.ExtractMibs(mibPath);

      return AgentExtractor.ExtractServer(this.ServerPath);
    }

    /// <summary>
    /// Log std err if <see cref="TraceStdErr"/> is set to true.
    /// </summary>
    /// <param name="sender">
    /// The sending <see cref="Process"/>.
    /// </param>
    /// <param name="e">
    /// The data captured from mongod.exe stderr.
    /// </param>
    private void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e)
    {
      if (!this.TraceStdErr)
      {
        return;
      }

      if (!string.IsNullOrEmpty(e.Data))
      {
        Trace.WriteLine(string.Format("Err - {0}", e.Data));
      }
    }

    /// <summary>
    /// Log std out if <see cref="TraceStdOut"/> is set to true.
    /// </summary>
    /// <param name="sender">
    /// The sending <see cref="Process"/>.
    /// </param>
    /// <param name="e">
    /// The data captured from mongod.exe stdout.
    /// </param>
    private void ProcessOutputDataReceived(object sender, DataReceivedEventArgs e)
    {
      if (!this.TraceStdOut)
      {
        return;
      }

      if (!string.IsNullOrEmpty(e.Data))
      {
        Trace.WriteLine(string.Format("Ouput - {0}", e.Data));
      }
    }

    /// <summary>
    /// Renders the snmpd.exe arguments.
    /// </summary>
    /// <param name="serverPath">
    /// The server Path.
    /// </param>
    /// <returns>
    /// An argument string.
    /// </returns>
    private string RenderArguments(string serverPath)
    {
      string serverDir = Path.GetDirectoryName(serverPath);
      var mibDir = string.IsNullOrEmpty(serverDir)
                     ? "mibs"
                     : Path.Combine(serverDir, "mibs").Replace(Path.DirectorySeparatorChar, '/');

      var confDir = string.IsNullOrEmpty(serverDir)
                      ? "snmpd.config"
                      : Path.Combine(serverDir, "snmpd.config").Replace(Path.DirectorySeparatorChar, '/');

      var args = string.IsNullOrEmpty(serverDir)
                   ? "-Mmibs -csnmpd.config"
                   : string.Format("-M{0} -c{1}", mibDir, confDir);

      return args;
    }

    #endregion
  }
}